package io.zbus.data.server.js;

import java.io.File;
import java.io.FileReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import com.alibaba.fastjson.JSONObject;

import io.zbus.data.kit.JsKit;
 
public class ES6Monitor {

	private static Logger logger = LoggerFactory.getLogger(ES6Monitor.class);
	 
    private Map<WatchKey, Path> keyPathMap = new HashMap<>();
    private ScriptEngine jsEngine = JsKit.getScriptEngine();
    private String func = "$compile";
    private Executor thread = Executors.newSingleThreadExecutor();
    private Path rootDir;

    private Path sourceDir;
    private String sourceDirAbsolutePath;
    private Path targetDir;
    private File babelFile; 
    private Path babelPath;
    private String targetDirAbsolutePath;
    private JavascriptInvoker jsInvoker;
    
    public void setJsInvoker(JavascriptInvoker jsInvoker) {
        this.jsInvoker = jsInvoker;
    }

    public ES6Monitor (String babelFile, String sourceDir, String targetDir) {
        Assert.notNull(sourceDir, "Source dir must not be null");
        Assert.notNull(targetDir, "Target dir must not be null");
        String root = System.getProperty("user.dir");
        this.rootDir = Paths.get(root);
        this.babelFile = this.rootDir.resolve(babelFile).toFile();
        this.babelPath = this.rootDir.resolve(babelFile).getParent();
        logger.info("Babel Path: "+this.babelFile);
        this.sourceDir = this.rootDir.resolve(sourceDir);
        this.targetDir = this.rootDir.resolve(targetDir);
        this.sourceDirAbsolutePath = this.sourceDir.toFile().getAbsolutePath();
        this.targetDirAbsolutePath = this.targetDir.toFile().getAbsolutePath();

        logger.info("Root: "+root);
        logger.info("Source: "+this.sourceDirAbsolutePath);
        logger.info("Target: "+this.targetDirAbsolutePath);
        thread.execute(() -> {
            try { 
                jsEngine.eval(new FileReader(babelFile));
                jsEngine.eval(String.join("\n", Arrays.asList(
                    "function " + func + "($input, $opts) { ",
                        "var opts = {};",
                        "$opts.keySet().forEach(function(k){",
                        "    opts[k] = $opts[k]",
                        "});",
                        "opts.presets = ['es2015', 'react', 'stage-0'];",
                        "//print(JSON.stringify(opts), '=>', $opts)",
                        "return Babel.transform($input, opts); ",
                    "}"
                )));
                try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
                    registerDir(this.sourceDir, watchService);
                    startListening(watchService);
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        });
    } 

    private void registerDir (Path path, WatchService watchService) throws Exception {
    	if(path.startsWith(babelPath)) return;
        if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
            // first compile
            compile(path);
            return;
        }

        logger.info("Monitoring: " + path); 
        WatchKey key = path.register(
            watchService,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY,
            StandardWatchEventKinds.ENTRY_DELETE
        );
        keyPathMap.put(key, path);
        for (File f : path.toFile().listFiles()) { 
            registerDir(f.toPath(), watchService);
        }
    }

    @SuppressWarnings("unchecked")
	private JSONObject transform(String es6, Map<String, Object> options) throws NoSuchMethodException, ScriptException {
        if (es6 == null || es6.trim() == "") {
            return null;
        }
        Invocable invocable = (Invocable) jsEngine;
        Object result = invocable.invokeFunction(func, es6, options);
		Map<String, Object> map = (Map<String, Object>)JsKit.convert(result);
        JSONObject json = new JSONObject(map);
        return json;
    }

    private void compile(Path source) throws Exception{
        File sourceFile = source.toFile();
        boolean isJs = sourceFile.getName().endsWith(".js");
        if (!sourceFile.isFile() || !isJs) { 
            return;
        } 
        try {
            
            Charset utf8 = StandardCharsets.UTF_8;
            List<String> lines = Files.readAllLines(source, utf8);
            if (lines == null || lines.isEmpty()) {
                return;
            }

            long start = System.currentTimeMillis();
            String sourceAbsolutePath = sourceFile.getAbsolutePath();
            String targetAbsolutePath = sourceAbsolutePath.replace(sourceDirAbsolutePath, targetDirAbsolutePath);
            Path target = Paths.get(targetAbsolutePath);
            File targetParent = target.getParent().toFile();
            if (!targetParent.exists()) {
                targetParent.mkdirs();
            } 

            logger.info("Compiling... "+ sourceAbsolutePath);
            String es6 = String.join("\n", lines);
            JSONObject json = null;
            Map<String, Object> options = new HashMap<>();
            options.put("ast", false);
            options.put("sourceMap", false); 
            try {
	            json = transform(es6, options);
            } catch (Throwable e) {
            	logger.warn("Compile "+sourceAbsolutePath+", Failed", e); 
            }
            if (json == null) {
                return;
            }
            String code = json.getString("code");
            // TODO source map 暂时用的有问题，等后续解决再开启
//            JSONObject sourceMap = json.getJSONObject("map");
//            String sourceURL = sourceAbsolutePath.replace(sourceDirAbsolutePath, "");
//            sourceMap.remove("sourcesContent");
            code = String.format(
//        		"//# sourceURL=%s\n%s\n//# sourceMappingURL=%s",
        		"//# sourceURL=%s\n%s",
        		targetAbsolutePath.replace("\\", "/"),
        		code
//        		sourceMapFilePath
    		);
            Files.write(target, code.getBytes(utf8));
//            Files.write(sourceMapPath, sourceMap.toJSONString().getBytes(utf8));
            
            long cost = System.currentTimeMillis()-start;
            logger.info("Compiled("+cost+"ms) "+targetAbsolutePath+", Success");

            if (this.jsInvoker != null) {
                String initJsPath = this.jsInvoker.getConfJsFilePath();
                String sysJsPath = this.jsInvoker.getProxyJsFilePath();
                int compare = Paths.get(initJsPath).compareTo(target);
                int compare2 = Paths.get(sysJsPath).compareTo(target);
                if (compare == 0 || compare2 == 0) {
                    // reload init
                    this.jsInvoker.init();
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    private void startListening (WatchService watchService) throws Exception {
        while (true) {
            WatchKey queuedKey = watchService.take();
            for (WatchEvent<?> watchEvent : queuedKey.pollEvents()) {
                Path path = (Path) watchEvent.context();
                //this is not a complete path
                //need to get parent path
                Path parentPath = keyPathMap.get(queuedKey);
                //get complete path
                path = parentPath.resolve(path);
                if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) { 
                    registerDir(path, watchService);
                } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    compile(path);
                } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
                    compile(path);
                }
            }
            if (!queuedKey.reset()) {
                keyPathMap.remove(queuedKey);
            }
            if (keyPathMap.isEmpty()) {
                break;
            }
        }
    } 
    
    public static void main(String[] args) {
        try {
            new ES6Monitor("es6/babel/babel6.min.js", "es6", "func");
            Thread.currentThread().join();
            System.out.println("System exist");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
